/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年10月19日
 */
package com.massyframework.beanskin.scriptengine.beancontext.provider;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.massyframework.beanskin.ModuleIdentifier;
import com.massyframework.beanskin.assembly.Assembly;
import com.massyframework.beanskin.assembly.AssemblyConfig;
import com.massyframework.beanskin.assembly.beans.BeanContextHandlerFactory;
import com.massyframework.beanskin.assembly.handling.Handler;
import com.massyframework.beanskin.service.ServiceRepository;
import com.massyframework.beanskin.util.IOUtils;
import com.massyframework.beanskin.util.classloading.ModuleClassLoader;
import com.massyframework.beanskin.util.classloading.ResourceLocateFilter;
import com.massyframework.beanskin.util.localstore.LocalStore;
import com.massyframework.beanskin.util.scanning.JarFileResourceScanner;
import com.massyframework.beanskin.util.scanning.ResourceScanner;
import com.massyframework.beanskin.util.scanning.ResourceScanningCallback;

/**
 * 基于Nashorn的JavaScript容器工厂
 * @author huangkh
 *
 */
final class ScriptEngnieContextFactory implements BeanContextHandlerFactory {
	
	private String PATH = "BEANSKIN-INF/js";
	private Logger logger = LoggerFactory.getLogger(ScriptEngnieContextFactory.class);
	private ScriptFileWatcher watcher;
	private List<ResourceScanner<?>> scanners;
	
	/**
	 * 构造方法
	 * @param watcher {@link ScriptFileWatcher}, 如果运行在生产环境
	 */
	public ScriptEngnieContextFactory(ScriptFileWatcher watcher) {
		this.watcher = Objects.requireNonNull(watcher, "\"watcher\" cannot be null.");
		this.scanners = new ArrayList<>();
		this.scanners.add(new JarFileResourceScanner());
		this.scanners.add(new JsDirectoryResourceScanner());
	}

	@Override
	public Handler createBeanContextHandler(Assembly assembly) {
		Objects.requireNonNull(assembly, "\"assembly\" cannot be null.");
		AssemblyConfig config = assembly.getConfig();

		Handler result = null;
		switch (config.getServletModel()) {
			case NONE:{
				result = new ScriptEngineContextInitalizer();
				break;
			}
		
			case LISTENER: {
				break;
			}
			case SERVLET: {
				result = new ScriptEngineServletContextInitializer();
				break;
			}
			
			default:{
				
			}
		}
		
		if (result != null) {
			Map<String, URL> fileMap = this.foundFiles(assembly);
			if (!fileMap.isEmpty()) {
				Set<Path> paths = this.copyToRuntimeDirectory(assembly, fileMap);
				this.watcher.addAssembly(assembly, paths);
			}
		}
		return result;
	}
	
	
	/**
	 * 复制文件到运行环境下对应目录
	 * @param assembly {@link Assembly},装配件
	 * @param fileMap {@link Map}, js目录下的所有文件
	 */
	protected Set<Path> copyToRuntimeDirectory(Assembly assembly, Map<String, URL> fileMap) {
		Set<Path> result = new HashSet<Path>();
		ServiceRepository repo = ServiceRepository.retrieveFrom(assembly);
		LocalStore localStore = repo.getService(LocalStore.class);
		
		Path path = localStore.getRuntimeDirectory(assembly).resolve("BEANSKIN-INF").resolve("js");
		for (Map.Entry<String, URL> entry: fileMap.entrySet()) {
			Path subPath = path.resolve(entry.getKey());
			Path parent = subPath.getParent();
			if (!result.contains(parent)) {
				if (!Files.exists(parent)) {
					try {
						Files.createDirectories(parent);
					} catch (IOException e) {
						if (logger.isErrorEnabled()) {
							logger.error("create directory failed:" +  parent, e);
						}
					}
				}
				result.add(parent);
			}
			
			if (Files.exists(subPath)) {
				continue;
			}
			
			//复制文件
			InputStream is = null;
			try {
				is = entry.getValue().openStream();
				Files.copy(is, subPath, StandardCopyOption.REPLACE_EXISTING);
			}catch(IOException e) {
				if (logger.isErrorEnabled()) {
					logger.error("copy failed:" +  entry.getValue().getPath(), e);
				}
			}finally {
				IOUtils.close(is);
			}
		}
		
		return result;
	}

	/**
	 * 发现{@code assembly}中BEANSKIN-INF/js目录下的文件
	 * @param assembly {@link Assembly},装配件
	 * @return {@link Map}, 发现的js目录下文件
	 */
	protected Map<String, URL> foundFiles(Assembly assembly) {
		Map<String, URL> result = new HashMap<>();
		
		ModuleClassLoader classLoader = assembly.getModuleClassLoader();
		try {
			Enumeration<URL> em = classLoader.getResources("BEANSKIN-INF/js/", new FilterImpl());
			while (em.hasMoreElements()) {
				URL url = em.nextElement();
				
				for (ResourceScanner<?> scanner: this.scanners) {
					this.doScanning(PATH, url, scanner, (path, res) -> {
						if (!classLoader.isDecryptScriptFile(PATH.concat("/").concat(path))) {
							if (result.putIfAbsent(path, res) != null) {
								if (logger.isWarnEnabled()) {
									logger.warn("found duplicate file path: path=" + path +", url=" + url.getPath() + ", skip.");
								}
							}
						}
					});
				}
			}
			
		}catch(Exception e) {
			if (logger.isWarnEnabled()) {
				logger.warn(e.getMessage(), e);
			}
		}
		
		return result;
	}
	
	/**
	 * 执行扫描
	 * @param <T>
	 * @param resourceDir {@link String},扫描的目录
	 * @param url {@link URL},相对路径
	 * @param scanner {@link ResourceScanner},资源扫描器
	 * @param callback {@link ResourceScanningCallback},资源扫描回调接口
	 */
	protected <T> void doScanning(String resourceDir, URL url, ResourceScanner<T> scanner, ResourceScanningCallback callback) {
		T instance = scanner.support(url);
		if (instance != null) {
			scanner.scanning(instance, resourceDir, name -> true, true, callback);
		}
	}
	
	/**
	 * 资源扫描过滤器
	 * @author huangkh
	 *
	 */
	class FilterImpl implements ResourceLocateFilter {

		@Override
		public String[] getResourceNames() {
			return null;
		}

		@Override
		public boolean isExclude(ModuleIdentifier moduleIdentifier) {
			return false;
		}
		
	}
}
